# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Exception classes used throughout gvn

Exception classes all inherit from either User or Internal, both of
which inherit from Root.  Internal errors represent programmer misuse
of gvn APIs; User errors represent external errors resulting from user
action, such as files in conflict, invalid server names, and so on.

All classes have a code attribute, which may be used as an exit code,
or other numeric error identifcation.  When gvn reaches 1.0, these
will not change, for compatibility.

All exception classes may be stringified (str(e) where e is an
exception object) to produce an error message.  User errors have a
diag_message property, which for some classes contains additional
diagnostic information to be shown if the user desires.  It is mainly
useful for debugging.  Since Internal represent programmer errors, the
normal stringification message includes any diagnostic information;
these have no diag_message property.

Some classes have other attributes, used to compute the error message
dynamically.  Only the final, outer-most gvn caller (e.g. the user
interface) should reset these attributes to affect that message.  See
each exception class for additional attributes.

A common example is the path attribute, which contains the internal
(unicode, unix path separator, relative to working copy root) form.
If the target of the operation was not the working copy root, callers
should reset this with the appropriate path (e.g. replace it with
gvn.cmdline.Context.DisplayPath(path)).
"""

# TODO(epg): Probably some of these are stale...

# TODO(epg): And of course all str here must be unicode, and
# gvn.cmdline.main needs to get the Context itself and encode before
# printing error messages.

import os
import platform

import svn.core


#: list of SubversionException.apr_err values which mean "out of date"
SVN_OUT_OF_DATE = [svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE,
                   # different code with ra-dav, oddly enough
                   svn.core.SVN_ERR_FS_CONFLICT]
# list of SubversionException.apr_err values which mean "no such
# path"; TODO(epg): we do this all over the place rather than using
# this, but it looks like the fix for this crap is going to make it
# into 1.5.0 so track it all down and remove it.
SVN_NOENT = [svn.core.SVN_ERR_FS_NOT_FOUND,
             svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND]


class Root(Exception):
  """Root of error class hierarchy

  Attributes:
  code          -- sys.exit code to use if caller considers the error fatal
  """
  code = None


class User(Root):
  """User errors are everything else, and not necessarily the user's
  fault: connection refused, auth errors, bad gvn command-line
  parameters, and so on.
  """
  diag_message = property(lambda self: str(self),
                       doc="""error message to use in debug/diagnostic mode""")


class BadOptions(User):
  """Exception to raise if options, or arguments to options, are invalid,
  inappropriate, missing, misused, abused, or subject to corporate bribery.
  """
  code = 2


class BadOperands(User):
  """Exception to raise if an operand is similarly troublesome (see
  GvnOptions's docstring).
  """
  code = 3


class InvalidChangeName(User):
  code = 4


class NotWC(User):
  code = 5
  def __init__(self, path):
    User.__init__(self, "'%s' is not a working copy" % (path,))


class Editor(User):
  code = 6
  def __init__(self, cmd, status):
    User.__init__(self, platform.DescribeSubprocessError(cmd, status))


class Unmodified(User):
  code = 7
  def __init__(self, status):
    User.__init__(self, 'not modified: ' + status)


class Conflict(User):
  """Tried to commit a path in conflict.

  Attributes:
  path       -- path in conflict, in internal form
  """
  code = 8
  def __init__(self, path):
    self.path = path
  def __str__(self):
    return "'%s' remains in conflict" % (self.path,)


class RepoPath(User):
  code = 9
  def __init__(self, path, revision):
    User.__init__(self,
                       "URL '%s' non-existent in revision %d" % (path,
                                                                 revision))

class NoProject(User):
  code = 10
  def __init__(self, project):
    User.__init__(self, 'No project for ' + project)


class ChangeIsPath(User):
  """Tried to use local file name to name a changebranch.

  Attributes:
  name       -- attempted changebranch name
  """
  code = 11
  def __init__(self, name):
    self.name = name
  def __str__(self):
    return ("Change '%s' is a path name; use '--force-change' to override"
            % (self.name,))


class ChangeIsC(User):
  code = 28
  def __init__(self):
    User.__init__(self,
                  ("Changebranch named 'c'; did you mean '--cc'?"
                   "  Use '--force-change' to override"))


class NoReviewers(User):
  code = 12
  def __init__(self):
    User.__init__(self,
                  "Must use --reviewers or fill in To field of the email form")


class NoUserChangeBranch(User):
  code = 13
  def __init__(self, user_path):
    User.__init__(self, ("User changebranch path '//%s' does not exist."
                         % (user_path,)))


class InvalidProjectName(User):
  code = 14
  def __init__(self, project):
    User.__init__(self, 'Project "' + project +
                        '" is not a valid project name.')


class NoChangeBranchBase(User):
  code = 15
  def __init__(self, base_path):
    User.__init__(self, ("project changebranch base '//%s' does not exist."
                         % (base_path,)))


class MixedPaths(User):
  code = 16
  def __init__(self):
    User.__init__(self, 'All must be absolute or relative')


class OutOfDateParent(User):
  """Tried to changebranch a path whose parent is out-of-date.

  Attributes:
  svn_error     -- svn.core.SubversionException object
  branch_path   -- path of changebranch relative to repository root
  revision      -- attempted copyfrom of changebranch
  change_path   -- path missing in revision relative to source branch
  """
  code = 18
  def __init__(self, svn_error, branch_path, revision, change_path):
    self.svn_error = svn_error
    self.branch_path = branch_path
    self.revision = revision
    self.change_path = change_path
  diag_message = property(lambda self: 'svn: %s\n'
                          'Tried to branch from %s@%d\n'
                          "but %s doesn't exist in that revision"
                          % (self.svn_error.args[0], self.branch_path,
                             self.revision, self.change_path))
  def __str__(self):
    return ("'%s/' is newer than '%s/'; try gvn update '%s'"
            % (self.change_path, self.branch_path, self.branch_path))


class BranchFromRoot(User):
  code = 19
  def __init__(self):
    User.__init__(self, 'Cannot branch from repository root')


class Mail(User):
  code = 20


class Cmdline(User):
  pass


class BadCommand(Cmdline):
  """Tried to use unknown command.

  Attributes:
  command    -- the unknown command
  """
  code = 21
  def __init__(self, command):
    self.command = command
  def __str__(self):
    return "Unknown command: '%s'" % (self.command,)


# TODO(epg): Use these two instead of BadOptions and BadOperands above.
# And document them.
# class BadOperands(Cmdline):
#   code = 22
#   def __init__(self, operands, message='Not enough arguments provided'):
#     self.operands = operands
#     self.message = message
#   def __str__(self):
#     return self.message % {'operands': operands}
# class BadOption(Cmdline):
#   code = 23
#   def __init__(self, option, message='invalid option: %(option)s'):
#     self.option = option
#     self.message = message
#   def __str__(self):
#     return self.message % {'option': self.option}


class UnknownEncoding(User):
  """Tried to use unknown encoding.

  Attributes:
  encoding      -- name of the unknown encoding
  """
  code = 24
  def __init__(self, encoding):
    self.encoding = encoding
  def __str__(self):
    return 'unknown encoding: %s' % (self.encoding,)


class Encoding(User):
  """Error encoding or decoding.

  No part of gvn raises this exception.  Instead, callers should catch
  UnicodeError and instantiate this class with that exception object
  as an argument.  This gives a much nicer error message.
  """
  code = 25
  def __init__(self, unicode_error):
    self._unicode_error = unicode_error
  def __str__(self):
    e = self._unicode_error
    if isinstance(e, UnicodeDecodeError):
      action = 'decode bytes'
    elif isinstance(e, UnicodeEncodeError):
      action = 'encode character'
    else:
      # Shouldn't be called with anything else, e.g. UnicodeTranslateError.
      return str(e)
    # Get everything up to but not including the bogon.
    preceding = e.object[:e.start]
    # Get the column number of the line of the bogon.
    column = e.start - preceding.rindex('\n')
    # Get the line number of the bogon; number of newlines + 1 because
    # preceding doesn't have the final newline (if any).
    line = preceding.count('\n') + 1
    # Get no more than 40 bytes of the line containing the bogon (or
    # the preceding line if the bogon started the line).
    preceding = preceding[-40:].splitlines()[-1]
    return ("'%s' codec can't %s at column %d of line %d; preceding text:\n%s"
            % (e.encoding, action, column, line, preceding))

class WCPath(User):
  code = 26
  def __init__(self, path):
    User.__init__(self, "Path '%s' does not exist" % (path,))

class ChangeBranchOutsideTarget(User):
  code = 27
  def __init__(self, cb_element_path, target_path):
    User.__init__(self, "Changebranch path '%s' is not in the "
                        "target path '%s'" % (cb_element_path, target_path,))


class NotVersioned(User):
  """Tried to operate on unversioned file.

  Attributes:
  path       -- unversioned path, in internal form
  """
  code = 29
  def __init__(self, path):
    self.path = path
  def __str__(self):
    return ("'%s' is not under version control"
            % (self.path,))


class Internal(Root):
  """Internal errors are caused by gvn bugs: improper use of svn (or
  an svn binding), one layer of gvn misusing another, and so on.
  """
  code = 100


# XXX I think we have multiple errors here, and maybe some are
# internal and some are User; i haven't looked yet.
class ChangeBranch(Internal):
  code = 105


class PathNotChild(Internal):
  code = 106
  def __init__(self, child, parent):
    Internal.__init__(self,
                           "'%s' not a child of '%s'" % (child, parent))


class WCReOpen(Internal):
  code = 107
  def __init__(self, write_lock, requested_write_lock,
               recursive, requested_recursive):
    Internal.__init__(self,
                           "WorkingCopy.Open mismatched write_lock (%s vs. %s) (%s vs. %s)"
                           % (write_lock, requested_write_lock, recursive, requested_recursive))


class WCClosed(Internal):
  code = 108


class NotShortURL(Internal):
  code = 109
  def __init__(self, url):
    Internal.__init__(self, "'%s' is not a short URL." % (url,))

class NotChangeBranched(Internal):
  code = 110
  def __init__(self, path):
    Internal.__init__(self, "'%s' not changebranched" % (path,))
    self.path = path

class ParseState(Internal):
  code = 111
  def __init__(self, child=None, message=None):
    self.child = child
    self.message = message
  def __str__(self):
    message = ['repository state of changebranch is invalid']
    if self.message is not None:
      message.append(self.message)
    if self.child is not None:
      message.append(str(self.child))
    return ': '.join(message)
